iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
JavaScript

Signal API in Angular系列 第 18

Day 18 - viewChild 函數的高階使用者案例 1 - 以程式設計方式建立 Angular 組件

  • 分享至 

  • xImage
  •  

viewChild 函數的一個高階用例是在 ViewConatinerRef 中建立 Angular 組件。。當我們知道應用程式載入期間不需要 Angular 組件時,我們可以將其延遲載入。優點是主包較小且初始載入時間較快。

在示範中,我以程式設計方式 (programmatically) 建立組件來顯示 jedi warriors 和 sith lords。 此示範使用 viewChild 函數查詢 ViewContainerRef 並呼叫其 createComponent 方法來動態建立組件。

引導應用程式

import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig = {
 providers: [
   provideHttpClient(),
   provideExperimentalZonelessChangeDetection()
 ]
}
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app.config';

bootstrapApplication(App, appConfig);

提供 Http client 和 experimental zoneless 功能,並引導應用程式設定。

新增獲取 StarWar 資料的函數

import { catchError, map, of, mergeMap, forkJoin } from 'rxjs';
import { inject, runInInjectionContext, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export type Person = {
 name: string;
 height: string;
 mass: string;
 hair_color: string;
 skin_color: string;
 eye_color: string;
 gender: string;
 films: string[];
}

const URL = 'https://swapi.dev/api/people';

export function getPerson(id: number, injector: Injector) {
 return runInInjectionContext(injector, () => {
   const http = inject(HttpClient);
   return http.get<Person>(`${URL}/${id}`).pipe(
     catchError((err) => {
       console.error(err);
       return of(undefined);
     }));
 });
}

getPerson 函數透過 id 檢索星際大戰角色。

建立一個 StarWarCharacterComponent

import { ChangeDetectionStrategy, Component, effect, inject, Injector, model, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { getPerson, Person } from './star-war.api';

@Component({
 selector: 'app-star-war-character',
 standalone: true,
 imports: [FormsModule],
 template: `
   <div class="border">
     @if(person(); as person) {
       <p>Id: {{ id() }} </p>
       @if (isSith()) {
         <p>A Sith, he is evil.</p>
       }
       <p>Name: {{ person.name }}</p>
       <p>Height: {{ person.height }}</p>
       <p>Mass: {{ person.mass }}</p>
       <p>Hair Color: {{ person.hair_color }}</p>
       <p>Skin Color: {{ person.skin_color }}</p>
       <p>Eye Color: {{ person.eye_color }}</p>
       <p>Gender: {{ person.gender }}</p>
     } @else {
       <p>No info</p>
     }
   </div>
 `,
})
export class AppStarWarCharacterComponent {
 injector = inject(Injector);
 id = model(1);
 isSith = model(false);
 person = signal<undefined | Person>(undefined);

 constructor() {
   effect((onCleanUp) => {
     const sub = getPerson(this.id(), this.injector)
       .subscribe((result) => {
         if (result) {
           const [person] = result;
           this.person.set(person);
         } else {
           this.person.set(undefined);
         }
       });

     onCleanUp(() => sub.unsubscribe());
   });
 }
}

AppStarWarCharacterComponent 有兩個 model inputsidisSith。 此組件使用 id model input 呼叫 Star War API 來檢索資料並將結果指派給 person signal。 當 isSith signal 為 true 時,它顯示 "A Sith, he is evil"。

新增 NgContainer 和下拉式清單以程式設計方式渲染組件

Component({
 selector: 'app-root',
 standalone: true,
 imports: [FormsModule],
 template: `
   <div class="container">
     <ng-container #vcr />
   </div>
   <select [(ngModel)]="jediId">
     <option value="1">Luke</option>
     <option value="10">Obi Wan Kenobe</option>
     <option value="20">Yoda</option>
   </select>
   <button (click)="addAJedi(jediId())">Add a Jedi</button>

   <select [(ngModel)]="sithId">
     <option value="4">Darth Vader</option>
     <option value="44">Darth Maul</option>
   </select>
   <button (click)="addAJedi(sithId(), true)">Add a Sith</button>`,
})
export class App implements OnDestroy {
 jediId = signal(1);
 sithId = signal(4);

 ngOnDestroy(): void {}
}

App 組件由 JediSith 下拉列表組成。 Jedi 列表的 NgModel 綁定到 jediId signal, Sith 列表的 NgModel 綁定到 sithId signal。當使用者點擊 "Add a Jedi" 按鈕時,組件會呼叫 addAJedi 方法將 AppStarWarCharacterComponent 附加到 ViewContainerRef。同樣,使用者點擊 "Add a Sith" 按鈕來呼叫相同的方法,將 AppStarWarCharacterComponent 組件附加到 ViewContainerRef

以程式設計方式建立 AppStarWarCharacterComponent

<ng-container #vcr />

NgContainer 有一個範本變數 vcrviewChild 函數使用它來查詢 ViewContainerRef

vcr = viewChild.required('vcr', { read: ViewContainerRef });

vcr 的類型是 Signal<ViewContainerRef>,因為 read 屬性會擷取 ViewContainerRef

componentRefs = [] as ComponentRef<any>[];

async addAJedi(id: number, isSith = false) {
   const { AppStarWarCharacterComponent } = await import ('./star-war/star-war-character.component');
   AppStarWarCharacterComponent
   const componentRef = this.vcr().createComponent(AppStarWarCharacterComponent);
   componentRef.instance.id.set(id);
   componentRef.instance.isSith.set(isSith);
   this.componentRefs.push(componentRef);
 }

addJedi 方法首先匯入 AppStarWarCharacterComponent。然後,createComponent 方法將組件附加到 ViewContainerRef 並傳回 ComponentRef reference。使用 idisSith 設定 componentRef.instance 的 model input。 將 componentRef 附加到 componentRefs 陣列, 在 ngDestroy lifecycle hook 中銷毀。

ngOnDestroy(): void {
   if (this.componentRefs) {
     for (const ref of this.componentRefs) {
       ref.destroy();
     }
   }
 }
``

當應用程式銷毀 `App` 組件時,`ngOnDestroy` 會釋放 `componentRefs` 的 memory 以避免 memory leaks。

## 結論:

- `viewChild` 可以查詢 `ViewContainerRef`,並且 `ViewContainerRef` 可以呼叫 `createComponent` 方法以程式設計方式附加組件。
- 組件可以設定 model input 來執行任何邏輯來更新其 HTML 範本。

鐵人賽的第 18 天就這樣結束了。

## 參考:

- Viewchild as signal: https://angular.dev/guide/signals/queries#viewchild
- Render a component programmatically: https://angular.dev/guide/components/programmatic-rendering#
- Demo: https://stackblitz.com/edit/stackblitz-starters-zuyqrt?file=src%2Fmain.ts

上一篇
Day 17 - viewChild 函數簡介
下一篇
Day 19 - viewChild函數的高階使用案例(二)- 將NgTemplate嵌入到ViewContainerRef中
系列文
Signal API in Angular39
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言